﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace SpeechRecognition
{
    /// <summary>
    /// Класс для реализации распознавания речи.
    /// </summary>
    public class Recognizer
    {
        public Recognizer() { }

        public Recognizer(double bound)
        {
            Bound = bound;
        }

        /// <summary>
        /// Количество октавных полос
        /// </summary>
        private const int octaveCount = 9;

        /// <summary>
        /// Значение порога для удаления латентных периодов.
        /// </summary>
        private double bound;

        /// <summary>
        /// Число отсчетов в преобразовании Лежандра.
        /// </summary>
        private const int sampleCount = 256;

        /// <summary>
        /// Частота среза
        /// </summary>
        private const double cutoffFrequency = 0.1;

        /// <summary>
        /// Половина числа точек, по которым строится фильтр
        /// </summary>
        private const int NP = 4;

        /// <summary>
        /// Количество точек, по которым смотрим изменения при сегментации
        /// </summary>
        private const int L = 6;

        /// <summary>
        /// Минимальное расстояние между максимумами при сегментации
        /// </summary>
        private const int KMin = 20;

        /// <summary>
        /// Устанавливает или задает значение порога.
        /// </summary>
        public double Bound
        {
            get { return bound; }
            set
            {
                if (value < 1.0 / 5 || value > 1.0 / 3)
                {
                    throw new ArgumentOutOfRangeException("Bound value must be in (0.2 ; 0.33) range!", (Exception)null);
                }
                else
                {
                    bound = value;
                }
            }
        }

        public List<short> SpeechSignal { get; private set; }

        public List<short> CutedSignal { get; private set; }

        public List<double> NormalizedSignal { get; private set; }

        /// <summary>
        /// Массив спектров, после преобразования полиномом Лежандра.
        /// </summary>
        public double[,] Spectrum { get; private set; }

        /// <summary>
        /// Массив октавных полос
        /// </summary>
        public double[,] OctaveBand { get; private set; }

        /// <summary>
        /// Массив фильтрованых октавных полос
        /// </summary>
        public double[,] FilteredOctaveBand { get; private set; }

        /// <summary>
        /// Массив для сегментации
        /// </summary>
        public double[] Segmentation { get; private set; }

        public List<int> SegmentationMaxValues { get; private set; }

        /// <summary>
        /// Метод для считывания WAV-файла.
        /// </summary>
        /// <param name="fileName">Имя файла.</param>
        public void ReadFromFile(string fileName)
        {
            SpeechSignal = new List<short>();
            FileInfo file = new FileInfo(fileName);
            using (BinaryReader br = new BinaryReader(file.OpenRead()))
            {
                br.BaseStream.Position = 88;
                while (br.BaseStream.Position != br.BaseStream.Length)
                {
                    SpeechSignal.Add(br.ReadInt16());
                }
            }
        }

        /// <summary>
        /// Метод выполняющий нормализацию исходного сигнала.
        /// </summary>
        public void Normalization()
        {
            double dispersion1 = SpeechSignal.Select(x => Math.Pow(x, 2)).Sum() / SpeechSignal.Count;
            double signalBound = bound * Math.Sqrt(dispersion1);
            int fromBegin = SpeechSignal.FindIndex(x => x > signalBound);
            int fromEnd = SpeechSignal.FindLastIndex(x => x > signalBound);
            CutedSignal = SpeechSignal.GetRange(fromBegin, fromEnd - fromBegin + 1);
            double dispersion2 = CutedSignal.Select(x => Math.Pow(x, 2)).Sum() / CutedSignal.Count;
            NormalizedSignal = CutedSignal.Select(x => (double)x / Math.Sqrt(dispersion1)).ToList();
        }

        /// <summary>
        /// Преобразование полиномом Лежандра.
        /// </summary>
        public void LegendreFunction()
        {
            int m = this.NormalizedSignal.Count / sampleCount;
            this.Spectrum = new double[m, sampleCount / 2];
            for (int k = 0; k < m; k++)
            {
                for (int n = 0; n < sampleCount / 2; n++)
                {
                    for (int i = 0; i < sampleCount; i++)
                    {
                        double x = (-0.5 + (double)i / (sampleCount));
                        double p = LegendrePolynom(n + 1, x);
                        this.Spectrum[k, n] += p * this.NormalizedSignal[sampleCount * k + i];
                    }
                }
            }

            for (int k = 0; k < m; k++)
            {
                for (int n = 0; n < sampleCount / 2; n++)
                {
                    this.Spectrum[k, n] = Math.Abs(this.Spectrum[k, n]) / sampleCount;
                }
            }

        }

        /// <summary>
        /// Вычисление октавных полос
        /// </summary>
        public void CalculateBands()
        {
            int lenght = this.Spectrum.GetLength(0);
            this.OctaveBand = new double[octaveCount, lenght];
            for (int i = 0; i < lenght; i++)
            {
                for (int k = 0; k < 5; k++)
                {
                    this.OctaveBand[k, i] = this.RootFromSpectrSum(i, k * 2, k * 2 + 1);
                }
                this.OctaveBand[5, i] = this.RootFromSpectrSum(i, 10, 14);
                this.OctaveBand[6, i] = this.RootFromSpectrSum(i, 15, 24);
                this.OctaveBand[7, i] = this.RootFromSpectrSum(i, 25, 49);
                this.OctaveBand[8, i] = this.RootFromSpectrSum(i, 50, 109);
            }
        }

        /// <summary>
        /// Вычисляет квадратный корень из сумм квадратов коефициентов спектра
        /// </summary>
        /// <param name="num">Номер спектра.</param>
        /// <param name="from">Номер первого коефициента.</param>
        /// <param name="to">Номер последнего коефициента.</param>
        /// <returns></returns>
        private double RootFromSpectrSum(int num, int from, int to)
        {
            double result = 0;
            for (int i = from; i <= to; i++)
            {
                result += Math.Pow(this.Spectrum[num, i], 2);
            }
            return Math.Sqrt(result);
        }

        /// <summary>
        /// Вычисление фильтрованых октавных полос
        /// </summary>
        public void CalculateFilteredBands()
        {
            int lenght = this.Spectrum.GetLength(0);
            this.FilteredOctaveBand = new double[octaveCount, lenght];
            for (int i = 0; i < octaveCount; i++)
            {
                this.CopyArrayValues(this.OctaveBand, this.FilteredOctaveBand, NP, i, 0);
                for (int n = NP; n < lenght - NP; n++)
                {
                    double filter = 0;
                    for (int k = -NP; k <= NP; k++)
                    {
                        filter += this.GetFilterCoefficient(k) * this.OctaveBand[i, n - k];
                    }
                    this.FilteredOctaveBand[i, n] = filter;
                }
                this.CopyArrayValues(this.OctaveBand, this.FilteredOctaveBand, NP, i, lenght - NP);
            }
        }

        /// <summary>
        /// Выполняем сегментацию
        /// </summary>
        public void MakeSegmentation()
        {
            this.Segmentation = new double[this.FilteredOctaveBand.GetLength(1) - L];
            for (int i = 0; i < this.Segmentation.Length; i++)
            {
                double temp = 0;
                for (int k = 0; k < this.FilteredOctaveBand.GetLength(0); k++)
                {
                    for (int index1 = i; index1 <= (i + 2); index1++)
                    {
                        for (int index2 = (i + 3); index2 <= (i + 5); index2++)
                        {
                            temp += Math.Pow(this.FilteredOctaveBand[k, index1] - this.FilteredOctaveBand[k, index2], 2);
                        }
                    }
                }
                this.Segmentation[i] = temp;
            }
            this.SegmentationMaxValues = new List<int>();
            this.FindMaxValues(this.Segmentation, 0, this.Segmentation.Length);
        }

        /// <summary>
        /// Ищем максимумы по массиву сегментации
        /// </summary>
        /// <param name="startIndex">Индекс начала поиска</param>
        /// <param name="endIndex">Индекс конца поиска</param>
        private void FindMaxValues(double[] array, int startIndex, int endIndex)
        {
            if (Math.Abs(endIndex - startIndex) > KMin)
            {
                int value = this.FindMax(array, startIndex, endIndex);                
                if (value != int.MinValue)
                {
                    this.SegmentationMaxValues.Add(value);
                    this.FindMaxValues(array, startIndex, value - 1);
                    this.FindMaxValues(array, value + 1, endIndex);
                }
            }
        }

        /// <summary>
        /// Ищем максимуму в заданном промежутке
        /// </summary>
        /// <param name="startIndex">Индекс начала поиска</param>
        /// <param name="endIndex">Индекс конца поиска</param>
        /// <returns></returns>
        private int FindMax(double[] array, int startIndex, int endIndex)
        {
            int res = int.MinValue;
            for (int i = startIndex + 1; i < endIndex - 1; i++)
            {
                if (res == int.MinValue)
                {
                    if (array[i] > array[i - 1] && array[i] > array[i + 1])
                    {
                        res = i;
                    }
                }
                else if (array[i] > array[res] && array[i] > array[i - 1] && array[i] > array[i + 1])
                {
                    res = i;
                }
            }
            return res;
        }

        /// <summary>
        /// Вычисляет коэффициент Ck
        /// </summary>
        /// <param name="k">Индекс для коэффициента.</param>
        /// <returns></returns>
        private double GetFilterCoefficient(int k)
        {
            if (k == 0)
            {
                return 2 * cutoffFrequency;
            }
            else
            {
                return Math.Sin(2 * Math.PI * k * cutoffFrequency) / (k * Math.PI);
            }
        }

        /// <summary>
        /// Копирует элементы двумерного массива
        /// </summary>
        /// <param name="sourceArray">Массив источник</param>
        /// <param name="destinationArray">Массив приемник</param>
        /// <param name="count">Количество измерений</param>
        /// <param name="dimension">Индекс строки в массивах</param>
        /// <param name="index">Индекс столбца в массивах, с которого начинается копирование</param>
        private void CopyArrayValues(double[,] sourceArray, double[,] destinationArray, int count, int dimension, int index)
        {
            for (int i = 0; i < count; i++)
            {
                destinationArray[dimension, index + i] = sourceArray[dimension, index + i];
            }
        }

        /// <summary>
        /// Метод вычисляет значение полинома Лежандра.
        /// </summary>
        /// <param name="n"></param>
        /// <param name="x"></param>
        /// <returns></returns>
        private double LegendrePolynom(int n, double x)
        {
            if (n < 0)
            {
                throw new ArgumentOutOfRangeException("n", "Степень полинома должна быть положительной!");
            }
            else if (n == 0)
            {
                return 1;
            }
            else if (n == 1)
            {
                return x;
            }
            else
            {
                double a = 1, b = x, result = 0;
                for (int i = 2; i <= n; i++)
                {
                    result = ((2 * i - 1) * x * b - (i - 1) * a) / i;
                    a = b;
                    b = result;
                }
                return result;
            }
        }
    }
}
